#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# Copyright (c) 2025 Li Auto Inc. and its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import ply.yacc as yacc
import argparse
import pprint
import sys
import os
import re

from ProfileLexer import tokens

#######################################################
import re
from jinja2 import FileSystemLoader, Environment

#######################################################

ProfileName = "TestProfile"

profile = dict()


def p_profile(p):
    """profile : profile init
               | profile deinit
               | profile include
               | profile case
               | init
               | deinit
               | include
               | case"""
    if len(p) == 2:
        result = p[1]
    else:
        result = p[2]

    if result is not None:
        key = result[0]
        val = result[1]

        if key == "cases":
            if key in profile:
                profile[key].append(val)
            else:
                profile[key] = [val]
        else:
            profile[key] = val

    p[0] = profile.copy()


def p_include(p):
    '''include : INCLUDE path_list SEMICOLON'''
    p[0] = ("include", p[2])


def p_path_list(p):
    """path_list : PATH
                 | path_list PATH"""
    if len(p) == 2:
        p[0] = [p[1]]
    elif len(p) == 3:
        p[0] = [*p[1], p[2]]


def p_init(p):
    """init : INIT COLON command SEMICOLON"""
    p[0] = ('init', p[3])


def p_deinit(p):
    """deinit : DEINIT COLON command SEMICOLON"""
    p[0] = ('deinit', p[3])


def p_command(p):
    '''command : LEFTBRACE STRING RIGHTBRACE
               | LEFTBRACE STRING ctrl_list RIGHTBRACE
               | LEFTBRACE STRING assert_list RIGHTBRACE
               | LEFTBRACE STRING assert_list ctrl_list RIGHTBRACE'''

    ctrl_list_default = {"wait": False, "device": False, "ld_lib":False}

    cmdline = p[2][1:-1].strip()
    if " " in cmdline:
        cmdline = re.sub(r'\s+', '    ', cmdline)

    if len(p) == 4:
        p[0] = {"action": cmdline, "assert":[]}
        p[0].update(ctrl_list_default)
    elif len(p) == 5:
        if isinstance(p[3], list):
            p[0] = {"action": cmdline, "assert":p[3]}
            p[0].update(ctrl_list_default)
        else:
            p[0] = {"action": cmdline, "assert":[]}
            p[0].update(p[3])
    else:
        p[0] = {"action": cmdline, "assert":p[3]}
        p[0].update(p[4])


def p_assert_list(p):
    '''assert_list : test_op STRING
                   | assert_list test_op STRING'''
    if len(p) == 3:
        p[0] = [(p[1], p[2][1:-1])]
    elif len(p) == 4:
        p[0] = [*p[1], (p[2], p[3][1:-1])]


def p_ctrl_list(p):
    '''ctrl_list : ctrl_wait
                 | ctrl_device
                 | ctrl_ld_lib STRING
                 | ctrl_wait TIMEOUT
                 | ctrl_sleep TIMEOUT
                 | ctrl_list ctrl_device
                 | ctrl_list ctrl_wait
                 | ctrl_list ctrl_wait TIMEOUT
                 | ctrl_list ctrl_ld_lib STRING
                 | ctrl_list ctrl_sleep TIMEOUT'''
    if len(p) == 2:
        p[0] = {p[1]: True}
    elif len(p) == 3:
        if re.match(r"^[1-9]\d*$", p[2]):
            p[0] = {p[1]: p[2]}
        elif re.match(r'\"(\\.|[^\\"\n])*\"',p[2]):
            path = p[2].replace('"', '')
            p[0] = {p[1]: path}
        else:
            p[0] = {p[2]: True}
            p[0].update(p[1])
    elif len(p) == 4:
        p[0] = {p[2]: p[3]}
        p[0].update(p[1])


def p_ctrl_wait(p):
    """ctrl_wait : CTRL_WAIT"""
    p[0] = "wait"


def p_ctrl_device(p):
    """ctrl_device : CTRL_DEVICE"""
    p[0] = "device"


def p_ctrl_sleep(p):
    """ctrl_sleep : CTRL_SLEEP"""
    p[0] = "sleep"

def p_ctrl_ld_lib(p):
    """ctrl_ld_lib : CTRL_LD_LIB"""
    p[0] = "ld_lib"


def p_case(p):
    '''case : command_list SEMICOLON
            | command_list plugin_list SEMICOLON'''
    if len(p) == 3:
        p[0] = ("cases", {"commands":p[1], "NAME":"UNKNOWN"})
    else:
        p[2]["commands"]= p[1]
        if "NAME" not in p[2]:
            p[2]["NAME"] = "UNKNOWN"

        p[0] = ("cases", p[2])


def p_command_list(p):
    '''command_list : command
                    | command_list COMMA command'''
    if len(p) == 2:
        p[0] = [p[1]]
    else:
        p[0] = [*p[1], p[3]]


def p_plugin_list(p):
    '''plugin_list : COLON NAME
                   | AT NAME
                   | plugin_list COLON NAME
                   | plugin_list AT NAME'''
    if len(p) == 3:
        if p[1] == ":":
            p[0] = {"NAME": p[2]}
        else:
            p[0] = {"TAG": [p[2]]}
    elif len(p) == 4:
        if p[2] == ':':
            p[1]["NAME"] = p[3]
            p[0] = p[1]
        else:
            if "TAG" in p[1]:
                p[1]["TAG"]= p[1]["TAG"] + '    ' + p[3]
            else:
                p[1]["TAG"] = p[3]

            p[0] = p[1]


def p_test_op(p):
    """test_op : op_equal
               | op_not_equal
               | op_contains
               | op_not_contains"""
    p[0] = p[1]


def p_op_equal(p):
    """op_equal : EQUAL
                | OP_EQUAL"""
    p[0] = "=="


def p_op_not_equal(p):
    """op_not_equal : NOT EQUAL
                    | OP_NOT_EQUAL"""
    p[0] = "!="


def p_op_contains(p):
    """op_contains : CONTAINS
                   | OP_CONTAINS"""
    p[0] = "~"


def p_op_not_contains(p):
    """op_not_contains : NOT CONTAINS
                       | OP_NOT_CONTAINS"""
    p[0] = "!~"

# Error rule for syntax errors


def p_error(p):
    error_line = p.lineno
    error_token = p.type
    error_token_value = p.value
    lexer = p.lexer
    error_pos = lexer.lexpos
    error_text = lexer.lexdata.strip()

    print("Syntax error at line {line}".format(line=error_line))
    print("Error position: {pos}".format(pos=error_pos))
    print("Error context: {context}".format(context=error_text))
    print("Unexpected token: ({token}, {value})".format(token=error_token, value=error_token_value))

    sys.exit(-1)


def read_file(path):
    try:
        f = open(path, encoding='UTF-8')
        data = f.read()
        f.close()

        return data
    except FileNotFoundError as err:
        print(err)
        return None


path_stack = []
history_path = []


def parse_profile(path):
    if os.path.isdir(path):
        path = os.path.realpath(path)
        profile_path = os.path.join(path, ProfileName)
    elif os.path.isfile(path):
        profile_path = os.path.realpath(path)
    else:
        return None
    if profile_path in history_path:
        return None
    history_path.append(profile_path)

    data = read_file(profile_path)
    if data is None:
        print("Couldn't handle '{0}'".format(profile_path))
        return None

    path_stack.append(os.path.realpath(os.path.curdir))
    os.chdir(os.path.dirname(profile_path))

    profile.clear()
    parser = yacc.yacc(debug=False, write_tables=False)
    try:
        ast = parser.parse(data)
    except Exception as e:
        print("Exception: {}, File {} does not contain valid data".format(e, profile_path))
        os.chdir(path_stack.pop())
        return None

    if "include" in ast:
        for item in ast["include"]:
            subProfile = os.path.join(os.path.curdir, item)

            subast = parse_profile(subProfile)
            if subast is not None:
                if "init" in subast:
                    ast["init"] = subast["init"]
                if "deinit" in subast:
                    ast["deinit"] = subast["deinit"]
                if "cases" in subast:
                    if "cases" in ast:
                        ast["cases"] += subast["cases"]
                    else:
                        ast["cases"] = subast["cases"]

        ast.pop("include")

    os.chdir(path_stack.pop())
    return ast.copy()


#############################################################################
class robot_case:
    def __init__(self):
        templatedirpath = os.path.split(os.path.realpath(__file__))[0] + "/templates"
        loader = FileSystemLoader(templatedirpath, encoding="utf-8")
        env = Environment(loader=loader, trim_blocks=False)

        self.tpl = env.get_template("robot_testcase.jinja")

    def render(self, case_list, command, platform):
        return self.tpl.render(case_list=case_list, flash_cmd=command,
                               platform=platform)


def ast_render(ast, platform):
    if ast == None:
        return ""

    case_list = ast.get("cases", None)

    if case_list == None:
        return ""

    command      = ast.get("init") if ast.get("init") else ""

    robot = robot_case()
    return robot.render(case_list, command, platform) + '\n'

#############################################################################


def main():
    option = argparse.ArgumentParser(description="Compiler for Test Profile")

    option.add_argument('--verbose', '-v',
                        action='store_true', help='verbose mode')
    option.add_argument('--platform', '-p',
                        action='store', dest="platform", default="LINUX",
                        help='Indicat the ALL Platform.')
    option.add_argument('--directory', '-c',
                        action='store', dest="topdir",
                        help='Change to directory.')
    option.add_argument('--file', '-f',
                        action='store', dest="profile",
                        help='Indicate a top profile.')
    option.add_argument('--output', '-o',
                        action='store', dest="output",
                        help='Indicate a output File.')
    option.add_argument('--input', '-i',
                        help='Indicate a input dir.')

    arg = option.parse_args()

    if arg.topdir is not None:
        try:
            os.chdir(arg.topdir)
        except OSError as err:
            print("Fail to change directoy: {0}".format(err))
            sys.exit(-1)

    if arg.input is None:
        top_profile = os.path.join(os.path.curdir, ProfileName)
    else:
        if not os.path.isfile(arg.input):
            print("'{0}' is not file or not exist.".format(arg.input))
            option.print_help()
            sys.exit(-2)

        top_profile = arg.input

    if arg.output is not None:
        try:
            output = open(arg.output, 'w')
        except FileNotFoundError as err:
            print(err)
            print("Couldn't open output file")
            option.print_help()
            sys.exit(2)
    else:
        output = sys.stdout

    ast = parse_profile(top_profile)
    print(arg.platform)

    buffer = ast_render(ast, arg.platform)

    if arg.verbose:
        print("Parser Result:\n\n")
        pprint.pprint(ast)
        print("Render Result:\n\n")
        print(buffer)

    if buffer is None:
        print("Fail to render\n")
        output.close()
        sys.exit(3)

    output.write(buffer)
    print("Generate testcases success: {0}".format(arg.output))
    output.close()


if __name__ == "__main__":
    main()
